﻿using System;
using System.Collections.Generic;
using System.IO;

namespace Chunker {
    public class ChunkEventArgs : EventArgs {
        public IChunk Chunk { get; set; }
    }

    public delegate void ChunkEventHandler(object sender, ChunkEventArgs e);

    /// <summary>
    /// Breaks a file into a list of chunks.
    /// </summary>
    public class ChunkController {
        /// <summary>
        /// Default window size.
        /// </summary>
        public const int WINDOW_SIZE = 48;

        /// <summary>
        /// Default min chunk size.
        /// </summary>
        public const int MIN_CHUNK_SIZE = 256;

        /// <summary>
        /// Default max chunk size.
        /// </summary>
        public const int MAX_CHUNK_SIZE = 512;

        /// <summary>
        /// Default DHash value.
        /// </summary>
        public const ulong DHASH = 1024;

        public event ChunkEventHandler ChunkCreated;
        public virtual void OnChunkCreated(object sender, ChunkEventArgs e) {
            if (ChunkCreated != null) {
                ChunkCreated(sender, e);
            }
        }

        /// <summary>
        /// Creates a file block list.
        /// This method uses the <code>WINDOW_SIZE</code>, <code>MIN_CHUNK_SIZE</code> and  <code>MAX_CHUNK_SIZE</code> as default values.
        /// </summary>
        /// <param name="filePath">The file path.</param>
        /// <returns>The file block list.</returns>
        public static IList<IChunk> GetChunkList<T>(string filePath)
            where T : IChunk {
            return GetChunks<T>(filePath, WINDOW_SIZE, MIN_CHUNK_SIZE, MAX_CHUNK_SIZE, DHASH);
        }

        /// <summary>
        /// Creates a file block list.
        /// </summary>
        /// <param name="filePath">The file path.</param>
        /// <param name="windowSize">Window size.</param>
        /// <param name="maxChunkSize">Maximun chunk size.</param>
        /// <returns>The file block list.</returns>
        public static IList<IChunk> GetChunks<T>(string filePath, int windowSize, int minChunkSize, int maxChunkSize, ulong dhash)
            where T : IChunk {
            IList<IChunk> chunkList = new List<IChunk>();

            ChunkController chunker = new ChunkController();
            chunker.ChunkCreated += (sender, e) => {
                chunkList.Add(e.Chunk);
            };
            chunker.ChunkFile<T>(filePath, windowSize, minChunkSize, maxChunkSize, dhash);

            return chunkList;
        }

        public void ChunkFile<T>(string filePath)
            where T : IChunk {
            ChunkFile<T>(filePath, WINDOW_SIZE, MIN_CHUNK_SIZE, MAX_CHUNK_SIZE, DHASH);
        }

        /// <summary>
        /// Breaks the file into chunks and fires an event for earch chunk created.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="filePath"></param>
        /// <param name="windowSize"></param>
        /// <param name="minChunkSize"></param>
        /// <param name="maxChunkSize"></param>
        /// <param name="dhash"></param>
        public void ChunkFile<T>(string filePath, int windowSize, int minChunkSize, int maxChunkSize, ulong dhash)
            where T : IChunk {
            byte[] window = new byte[windowSize];
            long lastPosition = 0;
            long currentPosition = 1;
            long bytes = -1;
            long windowOffset = 0;

            using (BufferedStream stream = new BufferedStream(new FileStream(filePath, FileMode.Open))) {
                currentPosition = bytes = stream.Read(window, 0, windowSize);
                windowOffset++;

                while (bytes != 0) {
                    long currentSize = currentPosition - lastPosition;

                    if (currentSize >= minChunkSize) {
                        ulong fingerPrint = RabinHash.ComputeHash(window);

                        // We've found a breakpoint or hitted the maximun chunk size
                        if (fingerPrint % dhash == (dhash - 1) || currentSize >= maxChunkSize) {
                            IChunk chunk = ChunkController.CreateChunk<T>(lastPosition, currentPosition, stream);
                            OnChunkCreated(this, new ChunkEventArgs { Chunk = chunk });
                            lastPosition = currentPosition;
                        }
                    }

                    stream.Seek(windowOffset, SeekOrigin.Begin);
                    bytes = stream.Read(window, 0, windowSize);

                    // We have reached the end of the file,
                    // let's creat a block with it!
                    if (bytes < windowSize) {
                        stream.Seek(lastPosition, SeekOrigin.Begin);
                        IChunk chunk = CreateChunk<T>(lastPosition, stream.Length, stream);
                        OnChunkCreated(this, new ChunkEventArgs { Chunk = chunk });
                        break;
                    }

                    currentPosition++;
                    windowOffset++;
                }
            }
        }

        /// <summary>
        /// Creates a new chunk
        /// </summary>
        /// <param name="startPosition"></param>
        /// <param name="endPosition"></param>
        /// <param name="stream"></param>
        /// <returns></returns>
        private static IChunk CreateChunk<T>(long startPosition, long endPosition, BufferedStream stream)
            where T : IChunk {
            // read the block content from stream.
            byte[] content = new byte[endPosition - startPosition];
            stream.Seek(startPosition, SeekOrigin.Begin);
            stream.Read(content, 0, content.Length);

            // infoke default constructor without parameters.
            IChunk chunk = typeof(T).GetConstructor(new Type[] { }).Invoke(new object[] { }) as IChunk;
            chunk.Content = content;
            chunk.Start = startPosition;
            chunk.End = endPosition;
            chunk.Hash = Util.GetHash(content);

            return chunk;
        }
    }
}
